// Package arpresolve 使用 ARP 请求获取局域网内某个主机的MAC地址
package arpresolve

import (
	"net"
	"time"

	"gitee.com/sillyman/simpleUtil/network/ifaceutil"
	"gitee.com/sillyman/simpleUtil/network/macaddr"
	"gitee.com/sillyman/simpleUtil/network/pcapraw"
	"github.com/google/gopacket"
	"github.com/google/gopacket/layers"
	"github.com/pkg/errors"
)

// Resolve 发送 ARP 请求获取目标IP地址的MAC地址(可能需要管理员身份才能访问raw socket)
// 依赖 pcap 库
func Resolve(localIP, remoteIP net.IP, retries int) (mac macaddr.MAC, err error) {
	ifi, err := ifaceutil.GetIfaceByIP(localIP)
	if err != nil {
		return nil, err
	}

	// raw 读取和写入网络接口
	raw, err := pcapraw.New(localIP, 0x0806, "")
	if err != nil {
		return nil, err
	}
	defer raw.Close()

	var (
		closed      bool
		recvErrCh   = make(chan error)            // 接收错误
		remoteMacCh = make(chan net.HardwareAddr) // 远端MAC
	)

	// revc ARP reply
	go func(remoteMacCh chan<- net.HardwareAddr, recvErrCh chan<- error) {
		for !closed {
			pkt, err := raw.ReadGoPkt(time.Second)
			if err != nil {
				recvErrCh <- err
				return
			}
			if pkt == nil {
				continue
			}

			if a := pkt.Layer(layers.LayerTypeARP); a != nil {
				reply := a.(*layers.ARP)

				if reply.Operation == layers.ARPReply && net.IP(reply.SourceProtAddress).Equal(remoteIP) {
					remoteMacCh <- net.HardwareAddr(reply.SourceHwAddress)
					return
				}
			}
		}
	}(remoteMacCh, recvErrCh)

	// send ARP request
	arpReqPktData := newArpReqPktData(ifi, localIP, remoteIP)
	tick := time.NewTicker(time.Second)
	defer tick.Stop()
	for i := 0; i < retries; i++ {
		select {
		case err := <-recvErrCh:
			return nil, err
		case remoteMAC := <-remoteMacCh: // ok
			return macaddr.New(remoteMAC), nil

		default:
			if err := raw.WriteRaw(arpReqPktData); err != nil {
				closed = true
				return nil, err
			}
			<-tick.C
		}
	}

	closed = true
	return nil, errors.New("received no ARP reply")
}

// newArpReqPktData 创建arp请求报文
func newArpReqPktData(ifi *net.Interface, srcIP, dstIP net.IP) []byte {
	var (
		buffer           = gopacket.NewSerializeBuffer()
		serializeOptions = gopacket.SerializeOptions{ComputeChecksums: false, FixLengths: false}
	)

	arpReq := &layers.ARP{
		AddrType:          layers.LinkTypeEthernet,
		Protocol:          layers.EthernetTypeIPv4,
		HwAddressSize:     uint8(6),
		ProtAddressSize:   uint8(4),
		Operation:         layers.ARPRequest,
		SourceHwAddress:   ifi.HardwareAddr,
		SourceProtAddress: srcIP.To4(),
		DstHwAddress:      macaddr.Zero.HardwareAddr(),
		DstProtAddress:    dstIP.To4(),
	}
	arpReq.SerializeTo(buffer, serializeOptions)
	buffer.PushLayer(arpReq.LayerType())

	eth := &layers.Ethernet{
		SrcMAC:       ifi.HardwareAddr,
		DstMAC:       macaddr.Bcast.HardwareAddr(),
		EthernetType: layers.EthernetTypeARP,
	}
	eth.SerializeTo(buffer, serializeOptions)
	buffer.PushLayer(eth.LayerType())

	return buffer.Bytes()
}
